package com.stylefeng.guns.filter;

import com.alibaba.fastjson.JSONObject;
import com.stylefeng.guns.core.base.tips.ErrorTip;
import com.stylefeng.guns.core.common.constant.JwtConstants;
import com.stylefeng.guns.core.common.exception.BizExceptionEnum;
import com.stylefeng.guns.core.shiro.JWTToken;
import com.stylefeng.guns.core.util.JwtTokenUtil;
import com.stylefeng.guns.core.util.RenderUtil;
import com.stylefeng.guns.vo.ResponseData;
import io.jsonwebtoken.JwtException;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authz.UnauthorizedException;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.RequestMethod;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URLEncoder;

/**
 * Created with IntelliJ IDEA @Author 张亚林 496185951@qq.com @Description
 * preHandle->isAccessAllowed->isLoginAttempt->executeLogin @Date 2018-10-12 @Time 00:08
 */
public class JWTFilter extends BasicHttpAuthenticationFilter {
  private Logger logger = LoggerFactory.getLogger(this.getClass());

  /** 如果带有 token，则对 token 进行检查，否则直接通过
   * 这里我们详细说明下为什么最终返回的都是true，即允许访问
   * 例如我们提供一个地址 GET /article
   * 登入用户和游客看到的内容是不同的
   * 如果在这里返回了false，请求会被直接拦截，用户看不到任何东西
   * 所以我们在这里返回true，Controller中可以通过 subject.isAuthenticated() 来判断用户是否登入
   * 如果有些资源只有登入用户才能访问，我们只需要在方法上面加上 @RequiresAuthentication 注解即可
   * 但是这样做有一个缺点，就是不能够对GET,POST等请求进行分别过滤鉴权(因为我们重写了官方的方法)，但实际上对应用影响不大
   */
  @Override
  protected boolean isAccessAllowed(
      ServletRequest request, ServletResponse response, Object mappedValue)
      throws UnauthorizedException {
    // 判断请求的请求头是否带上 "Token"
    if (isLoginAttempt(request, response)) {
      // 如果存在，则进入 executeLogin 方法执行登入，检查 token 是否正确
      try {
        executeLogin(request, response);
        return true;
      } catch (Exception e) {
        // token 错误
        responseError(response, e.getMessage());
      }
    }
    // 如果请求头不存在 Token，则可能是执行登陆操作或者是游客状态访问，无需检查 token，直接返回 true
    return true;
  }

  /** 判断用户是否想要登入。 检测 header 里面是否包含 Token 字段 */
  protected boolean isLoginAttempt(HttpServletRequest request, HttpServletResponse response) {
    final String token = request.getHeader(JwtConstants.AUTH_HEADER);
    String authToken;
    if (token != null && token.startsWith("Bearer ")) {
      authToken = token.substring(7);

      // 验证token是否过期,包含了验证jwt是否正确
      try {
        boolean flag = JwtTokenUtil.isTokenExpired(authToken);
        if (flag) {
          RenderUtil.renderJson(
                  response,
              new ErrorTip(
                  BizExceptionEnum.TOKEN_EXPIRED.getCode(),
                  BizExceptionEnum.TOKEN_EXPIRED.getMessage()));
          return false;
        }
      } catch (JwtException e) {
        // 有异常就是token解析失败
        RenderUtil.renderJson(
                response,
            new ErrorTip(
                BizExceptionEnum.TOKEN_ERROR.getCode(), BizExceptionEnum.TOKEN_ERROR.getMessage()));
        return false;
      }
    } else {
      // header没有带Bearer字段
      RenderUtil.renderJson(
              response,
          new ErrorTip(
              BizExceptionEnum.TOKEN_ERROR.getCode(), BizExceptionEnum.TOKEN_ERROR.getMessage()));
      return false;
    }
    return true;
  }

  /** 执行登陆操作 */
  @Override
  protected boolean executeLogin(ServletRequest request, ServletResponse response)
      throws Exception {
    HttpServletRequest httpServletRequest = (HttpServletRequest) request;
    String token = httpServletRequest.getHeader(JwtConstants.AUTH_HEADER);
    JWTToken jwtToken = new JWTToken(token);
    // 提交给realm进行登入，如果错误他会抛出异常并被捕获
    getSubject(request, response).login(jwtToken);
    // 如果没有抛出异常则代表登入成功，返回true
    return true;
  }

  /** 对跨域提供支持 */
  @Override
  protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
    HttpServletRequest httpServletRequest = (HttpServletRequest) request;
    HttpServletResponse httpServletResponse = (HttpServletResponse) response;
    httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
    httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
    httpServletResponse.setHeader("Access-Control-Allow-Headers",httpServletRequest.getHeader("Access-Control-Request-Headers"));

   
    // 跨域时会首先发送一个option请求，这里我们给option请求直接返回正常状态
    if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
      httpServletResponse.setStatus(HttpStatus.OK.value());
      return false;
    }
    return super.preHandle(request, response);
  }

  /** 将非法请求跳转到 /unauthorized/** */
  private void responseError(ServletResponse response, String message) {
    try {
      HttpServletResponse httpServletResponse = (HttpServletResponse) response;
      // 设置编码，否则中文字符在重定向时会变为空字符串
      message = URLEncoder.encode(message, "UTF-8");
      httpServletResponse.sendRedirect("/unauthorized/" + message);
    } catch (IOException e) {
      logger.error(e.getMessage());
    }
  }
  
 
  /**
   * 鉴定失败，返回错误信息
   * @param token
   * @param e
   * @param request
   * @param response
   * @return
   */
  @Override
  protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) {
    try {
      ((HttpServletResponse) response).setStatus(HttpStatus.BAD_REQUEST.value());
      response.getWriter().print("账号活密码错误");
    } catch (IOException e1) {
        logger.error(e1.getMessage(), e1);
    }
    return false;
  }
  
  /**
   * token 认证失败
   *
   * @param response
   */
  private void onLoginFail(ServletResponse response) {
    HttpServletResponse httpResponse = (HttpServletResponse) response;
    ((HttpServletResponse) response).setStatus(HttpStatus.UNAUTHORIZED.value());
    try {
      response.getWriter().print("没有权限，请联系管理员授权");
    } catch (IOException e) {
        logger.error(e.getMessage(), e);
    }
  }
  
  //请求不通过，返回错误信息给客户端
  private void responseMessage(HttpServletResponse response, PrintWriter out, ResponseData responseData) {
    responseData = ResponseData.forbidden();
    response.setContentType("application/json; charset=utf-8");
    String json = JSONObject.toJSONString(responseData);
    out.print(json);
    out.flush();
    out.close();
  }
}
